/**
 * @use(lastfmbackup.Language)
 */
qx.Class.define("lastfmbackup.model.Storage", {
  
  extend : qx.core.Object,

  properties : {
    
    username : {
      check        : "String",
      deferredInit : true,
      apply        : "_applyUsername"
    },
    
    stats : {
      check    : "Map",
      nullable : true,
      apply    : "_applyStats",
      event    : "changeStats"
    },
    
    chunkLength : {
      check : "Number",
      init  : 8192
    },
    
    saveProgress : {
      check : "Number",
      init  : 0,
      event : "changeSaveProgress"
    }
    
  },
  
  events : {

    /**
     *  Fired when model's async fetch method completes
     */
    fetch : "qx.event.type.Data",
    
    /**
     *  Fired when model's saved its new data
     */
    save : "qx.event.type.Event",
    
    /**
     *  Fired when model's failed to save its new data
     */
    fail : "qx.event.type.Event"
    
  },
  
  statics : {
    
    convertToMap : function(row)
    {
      return {
        "date"  : row[0], 
        "track" : {
          "name" : row[1],
          "mbid" : row[4]
        },
        "album" : {
          "name" : row[3],
          "mbid" : row[6]
        },
        "artist" : {
          "name" : row[2],
          "mbid" : row[5]
        }
      };
    },
    
    convertToRow : function(map)
    {
      return [
        map.date,
        map.track.name,
        map.artist.name,
        map.album.name,
        map.track.mbid,
        map.artist.mbid,
        map.album.mbid
      ];
    },
    
    versionCompare : function(a, b)
    {
      if(typeof a != "string" || typeof b != "string")
      {
        throw new Error("Invalid versions to compare");
      }
      
      var aParts = a.split(".");
      var bParts = b.split(".");

      var greater = function(ca, cb)
      {
        return (ca && !cb && parseInt(ca) > 0) || (parseInt(ca) > parseInt(cb));
      };
      for(var i = 0, l = Math.max(aParts.length, bParts.length); i < l; i++) 
      {
        if(greater(aParts[i], bParts[i])) 
        {
          return 1;
        } 
        else if(greater(bParts[i], aParts[i])) 
        {
          return -1;
        }
      }
      
      return 0;
    }
    
  },
  
  construct : function(username)
  {
    this.base(arguments);

    if(username)
    {
      this.initUsername(username);
    }
    
    this._nameRange = Array.range(1, 4);
    this._mbidRange = Array.range(4, 7);
    
    this._compressor = new lastfmbackup.model.Compression();
  },
  
  destruct : function()
  {
    this._nameRange = this._mbidRange = null;
    this._disposeObjects("_compressor");
  },
  
  members : {
    
    _minCompatibleWith : "0.3",
    
    _nameRange : null,
    _mbidRange : null,
    
    _compressor : null,
    _buffer     : null,
    
    
    _applyUsername : function(value)
    {
      var stats = qx.bom.Storage.getLocal().getItem(this._getStorageName("stats"));
      if(stats)
      {
        // storage format compatibility check
        var compared;
        try
        {
          compared = this.constructor.versionCompare(stats.formatVersion, this._minCompatibleWith);
        }
        catch(ex)
        {
          compared = -1;
        }
        
        if(compared == -1)
        {
          this.debug("Data cleared, version {0} < {1}".format(stats.formatVersion, this._minCompatibleWith));
          qx.bom.Storage.getLocal().clear();
        }
        else
        {
          this.setStats(stats);
        }
      }
      else
      {
        this.setStats(null);
      }
    },
    
    _applyStats : function(value)
    {
      var local = qx.bom.Storage.getLocal();
      if(value)
      {
        local.setItem(this._getStorageName("stats"), value);
      }
      else
      {
        local.removeItem(this._getStorageName("stats"));
      }
    },
    
    _getStorageName : function(type)
    {
      return "{0}.{1}.{2}".format(this.classname, this.getUsername(), type);
    },
    
    fetchJsonBlob : function()
    {
      this._fetchData(function(data)
      {
        var result = null;
        if(data)
        {
          var jsonText = qx.lang.Json.stringify(data);
          result = new window.Blob([jsonText], {"type" : "application/octet-stream"});
        }
        this.fireDataEvent("fetch", result);
      });
    },
    
    fetchCsvBlob : function()
    {
      this._fetchData(function(data)
      {
        var result = null;
        if(data)
        {
          var csvText = data.map(function(row)
          {
            return row.join("\t");
          }).join("\n");
          result = new window.Blob([csvText], {"type" : "application/octet-stream"});
        }
        this.fireDataEvent("fetch", result);
      });
    },
    
    _fetchUnchunked : function(callback)
    {
      this._compressor.addListenerOnce("complete", function(event)
      {
        var result = event.getData();
        
        callback.call(this, {
          "timeline" : qx.lang.Json.parse(result[0].data), 
          "nameDict" : qx.lang.Json.parse(result[1].data), 
          "mbidDict" : qx.lang.Json.parse(result[2].data)
        });
      }, this);

      var local = qx.bom.Storage.getLocal();
      
      ["data.timeline", "data.nameDict", "data.mbidDict"]
        .map(this._getStorageName, this)
        .map(local.getItem, local)
        .map(this._compressor.decompress, this._compressor);
      
      this._compressor.run();
    },
    
    _fetchData : function(callback)
    {
      this._fetchUnchunked(function(unchunked)
      {
        if(unchunked.timeline)
        {
          var local = qx.bom.Storage.getLocal();
          var index = 0;
          var chunk;
          while(chunk = local.getItem(this._getStorageName("data.rowset.{0}".format(index++))))
          {
            this._compressor.decompress(chunk);
          }

          this._compressor.addListenerOnce("complete", function(event)
          {
            var rowsetPlain = event.getData().reduce(function(a, b)
            {
              return a.concat(qx.lang.Json.parse(b.data));
            }, []);
          
            if(unchunked.timeline.length !== rowsetPlain.length)
            {
              throw new Error("Timline and rowset do not match");  
            }
            
            var nameRangeMap = qx.lang.Object.fromArray(this._nameRange);
            var mbidRangeMap = qx.lang.Object.fromArray(this._mbidRange);
            
            callback.call(this, unchunked.timeline
              .map(function(ts, index)
              {
                return [].concat(
                  [ts],
                  rowsetPlain[index].map(function(value, valueIndex)
                  {
                    valueIndex++;
                    if(valueIndex in nameRangeMap)
                    {
                      return unchunked.nameDict[value];
                    }
                    else if(valueIndex in mbidRangeMap)
                    {
                      return unchunked.mbidDict[value];
                    }
                    else
                    {
                      return null;                
                    }
                  }, this)
                );
              }, this)
              .sort(function(a, b)
              {
                return a[0] - b[0];
              })
            );
          }, this);
          
          this._compressor.run();
        }
        else
        {
          callback.call(this, null);
        }
      });
    },
    
    fetchData : function()
    {
      this._fetchData(function(data)
      {
        this.fireDataEvent("fetch", data);
      });
    },
    
    _storeCompressed : function(timeline, nameDict, mbidDict, rowsetChunked)
    {
      [timeline, nameDict, mbidDict]
        .concat(rowsetChunked)
        .map(qx.lang.Json.stringify)
        .map(this._compressor.compress, this._compressor);
      
      var onChangeProgressId = this._compressor.addListener("changeProgress", function(event)
      {
        this.setSaveProgress(event.getData());
      }, this); 
      var onCompleteId = this._compressor.addListenerOnce("complete", function(event)
      {
        var stats        = this.getStats();
        var chunkLength  = stats ? stats.rowsetChunkLength : 0;
        var local        = qx.bom.Storage.getLocal();
        var storageUsed  = 0;
        var storageNames = ["data.timeline", "data.nameDict", "data.mbidDict", "data.rowset.{0}"];
        
        event.getData().forEach(function(item, index)
        {
          var name = index < 3 ? storageNames[index] : storageNames[3].format(chunkLength + index - 3);
          
          local.setItem(this._getStorageName(name), item.data);
          storageUsed += item.data.length;
        }, this);
      
        this.setStats({
          "lastRun"           : Date.now(),
          "firstScrobble"     : timeline[0] * 1000,
          "lastScrobble"      : timeline[timeline.length - 1] * 1000,
          "scrobbles"         : timeline.length,
          "storageUsed"       : storageUsed,
          "formatVersion"     : qx.core.Environment.get("lastfmbackup.version"),
          "rowsetChunkLength" : chunkLength + rowsetChunked.length
        });
        
        this._compressor.removeListenerById(onFailId);
        this._compressor.removeListenerById(onChangeProgressId);
        
        this.fireEvent("save");
      }, this);
      var onFailId = this._compressor.addListenerOnce("fail", function()
      {
        this._compressor.removeListenerById(onCompleteId);
        this._compressor.removeListenerById(onChangeProgressId);
        
        this.fireEvent("fail");
      }, this);
      
      this._compressor.run();
    },
    
    saveData : function(rowArray)
    {
      if(!rowArray || !rowArray.length)
      {
        return this.fireEvent("save");
      }
      
      this.setSaveProgress(0);
      
      this._fetchUnchunked(function(unchunked)
      {
        // assume scrobble timestamp unique
        var timeline    = unchunked.timeline || [];
        var timelineSet = qx.lang.Object.fromArray(timeline);
        
        // filter duplication in newcomming data, sort by timestamp
        rowArray = rowArray.filter(function(row)
        {
          return !(row[0] in timelineSet);
        }).sort(function(a, b)
        {
          return a[0] - b[0];
        });
        
        // resulting timeline
        timeline = timeline.concat(rowArray.map(function(row)
        {
          return row[0];
        }));
        
        var listToDict = function(list)
        {
          return Object.dict(list.map(function(value, index)
          {
            return [value, index];
          }));
        };
        
        // create/update name and mbid dicts
        var nameDict, nameId, mbidDict, mbidId;
        if(unchunked.nameDict && unchunked.mbidDict)
        {
          nameDict = listToDict(unchunked.nameDict);
          nameId   = unchunked.nameDict.length;
          mbidDict = listToDict(unchunked.mbidDict);
          mbidId   = unchunked.mbidDict.length;
        }
        else
        {
          nameDict       = {};
          nameDict[null] = 0;
          nameId         = 1;
          mbidDict       = {};
          mbidDict[null] = 0;
          mbidId         = 1;
        }
        
        // rowset being appended
        var rowsetChunked = [];
        rowsetChunked.push([]);
        
        // fill rowset, update dicts 
        rowArray.forEach(function(row)
        {
          var newRow = [];
          
          this._nameRange.forEach(function(i)
          {
            if(!(row[i] in nameDict))
            {
              nameDict[row[i]] = nameId++;
            }
            newRow[i - 1] = nameDict[row[i]];
          }, this);
          
          this._mbidRange.forEach(function(i)
          {
            if(!(row[i] in mbidDict))
            {
              mbidDict[row[i]] = mbidId++;
            }
            newRow[i - 1] = mbidDict[row[i]];
          }, this);
          
          // chunk rowset
          if(rowsetChunked[rowsetChunked.length - 1].length >= this.getChunkLength())
          {
            rowsetChunked.push([]);
          }
          rowsetChunked[rowsetChunked.length - 1].push(newRow);
        }, this);
        
        var dictToList = function(dict)
        {
          var result = Object.items(dict).sort(function(a, b)
          {
            return a[1] - b[1];
          }).map(function(pair)
          {
            return pair[0];
          });
          // "null" -> null
          result[0] = null;
          
          return result;
        };
        
        this._storeCompressed(timeline, dictToList(nameDict), dictToList(mbidDict), rowsetChunked);
      });
    },
    
    resetData : function()
    {
      var local = qx.bom.Storage.getLocal(); 
      
      ["data.timeline", "data.nameDict", "data.mbidDict"]
        .map(this._getStorageName, this)
        .map(local.removeItem, local);

      var stats = this.getStats();
      if(stats)
      {
        Array.range(stats.rowsetChunkLength).forEach(function(index)
        {
          local.removeItem(this._getStorageName("data.rowset.{0}".format(index))); 
        }, this);
      }
        
      this.resetStats();
    },
    
    appendData : function(mapArray)
    {
      this._buffer = this._buffer || [];
      this._buffer = this._buffer.concat(mapArray.map(this.constructor.convertToRow, this.constructor));
    },
    
    flushData : function()
    {
      this.saveData(this._buffer || []);
    }
    
  }
  
});